#include "memory_handler.h"

extern int config_values[CONFIG_NUM];

extern int cmd_count;

FILE * memory_file;

int freeCache(Cache * cache);
int loadMemoryLine(char * memory_line);

int mem_pos;

unsigned char memory[MAX_MEMORY_SIZE] = {0x0};

int total_memory_access_time;
int total_memory_accesses;

int parseAddressForCache(Cache * cache, int address, int * tag, int * index, int * offset);
int alignAddress(Cache * cache, int address);
int restoreAddress(Cache * cache, int tag, int index, int offset);
int isInSameBlock(Cache * cache, int address1, int address2);
int isInCache(Cache * cache, int address, int * way, int * index);
int updateLRU(Cache * cache, int index, int lastUsedWay);
int copyWord(unsigned char * target, int target_offset, unsigned char * source, int source_offset);
int copyWordFromCache(Cache * cache, int way, int address, unsigned char * data, int target_offset);
int writeBlockToMemory(Cache * cache, int address, int way, int index);
int getWordFromCache(Cache * cache, int address, unsigned char * target);
int writeWordToCache(Cache * cache, unsigned char * word, int address);

int decrementBusyCycles(Cache * cache, int amount);
int decrementAllBusyCycles(int amount);
int isBusy(Cache * cache);
int calculateCyclicDistance(Cache * cache, int address1, int address2);

void advanceTime(int amount);

int resetCacheAccessInfo();

int readOperationTiming(int * accessTime);
int writeOperationTiming(int * accessTime);
int cacheTimeAlgorithm();

double calculateHitRate(Cache * cache1, Cache * cache2);


//=====================================================================================================================
//MAIN MEMORY
//=====================================================================================================================

int resetAMAT() {
	total_memory_accesses = 0;
	total_memory_access_time = 0;
	return 0;
}


int loadMemoryFile(char * file_path) {

	int file_size;
	int num_lines;

	char memory_line[CHARS_IN_LINE];

	memory_file = fopen(file_path, "r");
	if (memory_file == NULL)
		return -1;

	printf("Loading memory initialization file...\n");
	file_size = file_length(memory_file);
	num_lines = file_size / CHARS_IN_LINE;
	memory_size = num_lines * BYTES_PER_LINE;

	mem_pos = 0;
	
	while(fgets(memory_line, CHARS_IN_LINE, memory_file) != NULL) {
		
		if (trimwhitespace(memory_line)[0] == '\0')
			continue;

		loadMemoryLine(memory_line);
	}
	fclose(memory_file);
	
	return 0;
}

int loadMemoryLine(char * memory_line) {

	int i;
	int y;
	unsigned char x;

	for (i = 0; i < BYTES_PER_LINE; i++) {
		sscanf(memory_line + 3*i, "%X", &y);
		x = (unsigned char)y;
		*(memory + mem_pos) = x;
		mem_pos++;
	}

	return 0;
}

int printMemory(FILE * target) {
	int i, j;

	if (target == NULL) return -1;

	for (i = 0; i < memory_size; i+=BYTES_PER_LINE)  {
		for (j=0; j< BYTES_PER_LINE; j++) {
			fprintf(target, "%.2x", memory[i+j]);
			if (j != BYTES_PER_LINE - 1)
				fprintf(target, " ");
		}
		fprintf(target, "\n");
	}

	return 0;

}



//=====================================================================================================================
//CACHES INITIALIZATION
//=====================================================================================================================
int initializeCache(Cache * cache, Cache * nextLevelCache, int cache_size, int num_of_ways, int block_size, int access_delay, int policy, char * name) {
	
	int num_of_blocks;
	int i, j, k;
	int mask_dummy = -1;

	strcpy(cache->name, name);

	cache->policy = policy;

	num_of_blocks = (cache_size / NUM_OF_CACHES_PER_LEVEL) / num_of_ways / block_size;

	cache->num_of_ways = num_of_ways;
	cache->num_of_blocks_per_way = num_of_blocks;
	cache->block_size = block_size;

	cache->cache_ways = (CacheWay *) malloc (num_of_ways * sizeof(CacheWay));
	if (cache->cache_ways == NULL)
		return -1;

	for (i = 0; i < num_of_ways; i++) {
		
		cache->cache_ways[i].cache_blocks = (CacheBlock *) malloc (sizeof(CacheBlock) * num_of_blocks);
		if (cache->cache_ways[i].cache_blocks == NULL)
			return -1;

		for (j = 0; j < num_of_blocks; j++) {

			cache->cache_ways[i].cache_blocks[j].block_data = (unsigned char *) malloc (sizeof(unsigned char) * block_size);
			if (cache->cache_ways[i].cache_blocks[j].block_data == NULL)
				return -1;

			for (k = 0; k < block_size; k++) {
				cache->cache_ways[i].cache_blocks[j].block_data[k] = 0;
				cache->cache_ways[i].cache_blocks[j].dirty = 0;
				cache->cache_ways[i].cache_blocks[j].tag = 0;
				cache->cache_ways[i].cache_blocks[j].valid = 0;
			}
		}
	}

	cache->LRU = (int *) malloc (sizeof(int) * num_of_blocks);
	if (cache->LRU == NULL) 
		return -1;

	for (i = 0; i < num_of_blocks; i++) 
		cache->LRU[i] = 0;

	cache->access_delay = access_delay;
	cache->accesses = 0;
	cache->misses = 0;

	cache->nextLevelCache = nextLevelCache;

	cache->num_offset_bits = iLog2(cache->block_size);
	cache->num_index_bits = iLog2(cache->num_of_blocks_per_way);
	cache->num_tag_bits = NUM_BITS_IN_WORD - cache->num_offset_bits - cache->num_index_bits;

	cache->offset_mask = (mask_dummy << cache->num_offset_bits) ^ mask_dummy;
	cache->tag_mask = mask_dummy << (NUM_BITS_IN_WORD - cache->num_tag_bits);
	cache->index_mask = (mask_dummy << cache->num_offset_bits) ^ cache->tag_mask;


	cache->busy_cycles = 0;
	cache->last_requested_address = -1;
	cache->current_imported_address = -1;
	cache->start_address = -1;
	cache->end_address = -1;
	cache->num_words_in_block = cache->block_size / NUM_BYTES_IN_WORD;
	return 0;
}
int initializeCaches() {

	int result = 0;

	result += initializeCache(&L1_data_cache, &L2_data_cache, config_values[L1_CACHE_SIZE], L1_CACHE_WAYS, 
							config_values[L1_BLOCK_SIZE], config_values[L1_ACCESS_DELAY], WRITE_THROUGH, "L1_data");

	result += initializeCache(&L1_inst_cache, &L2_inst_cache, config_values[L1_CACHE_SIZE], L1_CACHE_WAYS,
							config_values[L1_BLOCK_SIZE], config_values[L1_ACCESS_DELAY], WRITE_THROUGH, "L1_inst");

	result += initializeCache(&L2_data_cache, NULL, config_values[L2_CACHE_SIZE], L2_CACHE_WAYS,
							config_values[L2_BLOCK_SIZE], config_values[L2_ACCESS_DELAY], WRITE_BACK, "L2_data");

	result += initializeCache(&L2_inst_cache, NULL, config_values[L2_CACHE_SIZE], L2_CACHE_WAYS, 
							config_values[L2_BLOCK_SIZE], config_values[L2_ACCESS_DELAY], WRITE_BACK, "L2_inst");


	if (result != 0)
	{
		freeCaches();
		return -1;
	}
	return 0;
}

int freeCache(Cache * cache) {

	int i, j;
	
	for (i = 0; i < cache->num_of_ways; i++) {
		
		for (j = 0; j < cache->num_of_blocks_per_way; j++) {
			free(cache->cache_ways[i].cache_blocks[j].block_data);
		}
		free(cache->cache_ways[i].cache_blocks);	
	}

	free(cache->cache_ways);
	free(cache->LRU);

	return 0;

}
int freeCaches() {

	freeCache(&L1_data_cache);
	freeCache(&L1_inst_cache);
	freeCache(&L2_data_cache);
	freeCache(&L2_inst_cache);

	return 0;
}


//=====================================================================================================================
//CACHE OPERATIONS
//=====================================================================================================================

int parseAddressForCache(Cache * cache, int address, int * tag, int * index, int * offset) {

	*offset = address & cache->offset_mask;
	*index = (address & cache->index_mask) >> cache->num_offset_bits;
	*tag = (address & cache->tag_mask) >> (cache->num_offset_bits + cache->num_index_bits);

	return 1;
}

int alignAddress(Cache * cache, int address) {
	return (address >> cache->num_offset_bits) << cache->num_offset_bits;
}

int restoreAddress(Cache * cache, int tag, int index, int offset) {

	int address = 0;

	tag = tag << (cache->num_offset_bits + cache->num_index_bits);
	index = index << cache->num_offset_bits;

	address = ((address ^ tag) ^ index) ^ offset;

	return address;

}

int isInSameBlock(Cache * cache, int address1, int address2) {
	if (alignAddress(cache,address1) == alignAddress(cache,address2))
		return 1;
	else
		return 0;
}

int isInCache(Cache * cache, int address, int * way, int * index) {

	int i;
	int tag;
	int offset;
	int my_way, my_index;
	int result = 0; //false
	
	my_way = 0;

	parseAddressForCache(cache, address, &tag, &my_index, &offset);

	for (i = 0; i < cache->num_of_ways; i++) {
		if ((cache->cache_ways[i].cache_blocks[my_index].tag == tag) && (cache->cache_ways[i].cache_blocks[my_index].valid == 1))
		{
			my_way = i;
			result = 1; //true
		}
	}

	if (way != NULL) 
		*way = my_way;
	if (index != NULL) 
		*index = my_index;

	return result;
}

/*This is an LRU implementation for 2-ways cache.
Generally, the code might support any number of ways.
In this case the data structure of Cache should be modified and save LRU bits for each block, not for each index.
*/
int updateLRU(Cache * cache, int index, int lastUsedWay) {

	if (cache->num_of_ways == 2) //do something only if cache has exactly 2 ways. For 1 way cache nothing happens.
		cache->LRU[index] = 1 - lastUsedWay;

	return 0;
}

int copyWord(unsigned char * target, int target_offset, unsigned char * source, int source_offset) {
	int i;
	for (i = 0; i < NUM_BYTES_IN_WORD; i++) {
		target[target_offset + i] = source[source_offset + i];
	}
	return 0;
}

int copyWordFromCache(Cache * cache, int way, int address, unsigned char * data, int target_offset) {

	int i;
	int tag;
	int index;
	int offset;

	parseAddressForCache(cache, address, &tag, &index, &offset);

	copyWord(data, target_offset, cache->cache_ways[way].cache_blocks[index].block_data, offset);

	return 0;
}

int importBlockIntoCache(Cache * cache, int address, int * levelsAccessed) {

	int target_cache_way, target_cache_index;
	int source_cache_way, source_cache_index;
	int target_tag, target_offset;
	int i, j, k, index;
	int mask = 0xFF;
	unsigned char * address_buffer;
	int startAddress, endAddress;
	int restoredAddress;

	startAddress = alignAddress(cache, address);
	endAddress = startAddress + cache->block_size - 1;

	cache->next_requested_address = address;

	if (cache_access_info.isInstruction) {
		address_buffer = (unsigned char *)malloc(NUM_BYTES_IN_WORD*sizeof(unsigned char));
		endAddress = (endAddress > inst_address(cmd_count)) ? inst_address(cmd_count) : endAddress;
	}

	parseAddressForCache(cache, startAddress, &target_tag, &target_cache_index, &target_offset);
	target_cache_way = cache->LRU[target_cache_index];

	*levelsAccessed = *levelsAccessed + 1;

	if (cache->nextLevelCache == NULL) //bring from memory
	{

		if (cache->cache_ways[target_cache_way].cache_blocks[target_cache_index].dirty) {
			cache_access_info.wasDirty = 1;
			restoredAddress = restoreAddress(cache, cache->cache_ways[target_cache_way].cache_blocks[target_cache_index].tag, target_cache_index, 0);
			writeBlockToMemory(cache,restoredAddress,target_cache_way,target_cache_index);
			cache->cache_ways[target_cache_way].cache_blocks[target_cache_index].dirty = 0;
		}
		for (i = startAddress, k = 0; i < endAddress; i+=NUM_BYTES_IN_WORD, k+=NUM_BYTES_IN_WORD) 
		{
			// if we accessed cache for instruction memory, then the instruction address is imported to the (instructions) cache
			if (cache_access_info.isInstruction) {
				for (j = 0 ; j < NUM_BYTES_IN_WORD ; j++) {
					address_buffer[j] = (i >> j*8) & mask;
				}
				copyWord(cache->cache_ways[target_cache_way].cache_blocks[target_cache_index].block_data,k,address_buffer,0);
			} else
				copyWord(cache->cache_ways[target_cache_way].cache_blocks[target_cache_index].block_data, k, memory, i);
		}

	}
	else //bring from next level cache
	{
		if (!isInCache(cache->nextLevelCache, address, &source_cache_way, &source_cache_index))
		{
			importBlockIntoCache(cache->nextLevelCache, address, levelsAccessed); //recoursive call
		}

		for (i = startAddress, target_offset = 0; i < endAddress; i+=NUM_BYTES_IN_WORD, target_offset+=NUM_BYTES_IN_WORD) 
		{
			copyWordFromCache(cache->nextLevelCache, source_cache_way, i, cache->cache_ways[target_cache_way].cache_blocks[target_cache_index].block_data, target_offset);
		}
		updateLRU(cache->nextLevelCache, source_cache_index, source_cache_way);
		
	}

	cache->start_address = startAddress;
	cache->end_address = endAddress;

	cache->cache_ways[target_cache_way].cache_blocks[target_cache_index].valid = 1;
	cache->cache_ways[target_cache_way].cache_blocks[target_cache_index].tag = target_tag;
	updateLRU(cache, target_cache_index, target_cache_way);

	if (cache_access_info.isInstruction && address_buffer != NULL)
		free(address_buffer);
	
	return 0;
}

int writeBlockToMemory(Cache * cache,int address,int way,int index) {
	int current_address, i;

	current_address = alignAddress(cache,address);

	for (i = 0 ; i < cache->block_size ; i+=NUM_BYTES_IN_WORD , current_address += NUM_BYTES_IN_WORD) {
		copyWordFromCache(cache,way,current_address,memory,current_address);
	}

	return 0;
}

int getWordFromCache(Cache * cache, int address, unsigned char * target) {

	int index, way, i;
	int accessedLevels = 0; // to know in how many cache levels there was miss

	Cache * CachePtr = cache;
	cache_access_info.L1_hit = 1; //we assume HIT

	cache_access_info.address = address;

	if (!isInCache(cache, address, &way, &index)) {
		cache_access_info.L1_hit = 0; //we got a MISS
		importBlockIntoCache(cache, address, &accessedLevels); // importing entire block from upper level cache
		
		cache_access_info.L2_hit = 1; //We assume L2 hit
		if (accessedLevels == 2) {
				cache_access_info.L2_hit = 0; //Accessed level = 2 => Memory accessed => L2 miss
		}
	}

	if (target != NULL) //Transfer NULL from instrution fetch
		copyWordFromCache(cache, way, address, target, 0);

	return 0;

}

int writeWordToCache(Cache * cache, unsigned char * word, int address) {
	
	int tag, index, offset, way, i;

	parseAddressForCache(cache,address,&tag,&index,&offset);

	isInCache(cache,address,&way,&index); // only to determine way

	copyWord(cache->cache_ways[way].cache_blocks[index].block_data, offset, word, 0);

	cache->cache_ways[way].cache_blocks[index].valid = 1;
	cache->cache_ways[way].cache_blocks[index].tag = tag;
	updateLRU(cache, index, way);

	
	switch (cache->policy) {

		case WRITE_THROUGH:
			writeWordToCache(cache->nextLevelCache, word, address); //recoursive call

		case WRITE_BACK:
			cache->cache_ways[way].cache_blocks[index].dirty = 1;

		default:
			break;
	}

	return 0;

}

//=====================================================================================================================
//FOR TIME CALCULATION
//=====================================================================================================================

int decrementBusyCycles(Cache * cache,int amount) {
	
	int cache_amount;

	cache_amount = amount;

	if (cache->busy_cycles - amount < 0) //if less than zero, equal to zero
		cache_amount = cache->busy_cycles;

	cache->busy_cycles -= cache_amount;

	//advance currently imported address, not relevant when cache is not busy
	cache->current_imported_address = advanceCurrentlyImportedAddress(cache, cache_amount); 

	return 0;
}

int decrementAllBusyCycles(int amount) {

	int write_buffer_amount;

	write_buffer_amount = amount;

	if (L1_data_cache.busy_cycles > 0) {
		decrementBusyCycles(&L1_data_cache,amount); }

	if (L1_inst_cache.busy_cycles > 0) {
		decrementBusyCycles(&L1_inst_cache,amount);	}

	if (L2_data_cache.busy_cycles > 0) {
		decrementBusyCycles(&L2_data_cache,amount);	}

	if (L2_inst_cache.busy_cycles > 0) {
		decrementBusyCycles(&L2_inst_cache,amount);	}

	
	if (write_buffer.busy_cycles - amount < 0)
		write_buffer_amount = write_buffer.busy_cycles; //if less than zero, equal to zero

	write_buffer.busy_cycles -= write_buffer_amount;

	//printf("DEBUG: write buffer busy cycles: %d\n",write_buffer.busy_cycles);


	return 0;
}

int isBusy(Cache * cache) {
	if (cache->busy_cycles > 0)
		return 1;
	return 0;
}

//Precondition: addresses are in same block
int calculateCyclicDistance(Cache * cache, int address1, int address2) {
	
	int address1_word_index;
	int address2_word_index;
	
	address1_word_index = (cache->offset_mask & address1) / NUM_BYTES_IN_WORD;
	address2_word_index = (cache->offset_mask & address2 ) /NUM_BYTES_IN_WORD;

	if (address1_word_index < address2_word_index)
		return (address2_word_index - address1_word_index);
	else
		return ((cache->num_words_in_block - address1_word_index - 1) + address2_word_index);
}

int advanceCurrentlyImportedAddress(Cache * cache, int amount) {

	int tmp_address;
	tmp_address = cache->current_imported_address + amount * NUM_BYTES_IN_WORD;

	if (tmp_address >= cache->end_address)
		tmp_address = tmp_address - cache->end_address + cache->start_address;

	return tmp_address;

	return 0;

}

int resetCacheAccessInfo() {

	cache_access_info.isInstruction = -1;
	cache_access_info.isWrite = -1;

	cache_access_info.L1_hit = -1;
	cache_access_info.L2_hit = -1;

	cache_access_info.wasDirty = 0;

	return 0;
}

//USED FOR DEBUG
void printBusyCycles() {
	printf("busy_cycles_L1_inst: %d\n", L1_inst_cache.busy_cycles);
	printf("busy_cycles_L1_data: %d\n", L1_data_cache.busy_cycles);

	printf("busy_cycles_L2_inst: %d\n", L2_inst_cache.busy_cycles);
	printf("busy_cycles_L2_data: %d\n", L2_data_cache.busy_cycles);
}

void advanceTime(int amount) {
	
	total_time += amount;
	//printBusyCycles();

	decrementAllBusyCycles(amount);
	
	//printf("DEBUG: time advanced by: %d\n",amount);

	return;

	
}

//Performs all the timing operations for MEMORY READ access
int readOperationTiming(int * accessTime) {

	Cache * L1CachePtr;
	Cache * L2CachePtr;
	
	int d1, d2, d, t; //used for local calculations; variable names exactly as in the algorithm in documentation
	t = 0;
	L1CachePtr = &L1_inst_cache; //assume instruction cache
	

	if (cache_access_info.isInstruction == 0) {
		L1CachePtr = &L1_data_cache; //fix to data cache
		*accessTime = *accessTime + write_buffer.busy_cycles;
		advanceTime(write_buffer.busy_cycles);
	}

	L2CachePtr = L1CachePtr->nextLevelCache;
	
	//L1

	if (cache_access_info.L1_hit) //L1 hit
	
	{ 

		//printf("L1 hit\n");
		if (isBusy(L1CachePtr)) {

			//printf("L1 busy\n");
			if (isInSameBlock(L1CachePtr, L1CachePtr->last_requested_address, cache_access_info.address)) {
				//printf("L1 busy same block\n");
				d1 = calculateCyclicDistance(L1CachePtr, L1CachePtr->last_requested_address, cache_access_info.address);
				d2 = calculateCyclicDistance(L1CachePtr, L1CachePtr->current_imported_address, cache_access_info.address);
				d = d1 - d2;
				if (d > 0) //word not yet arrived
				{
					//printf("L1 busy same block not arrived\n");
					*accessTime = *accessTime + d;
					advanceTime(d);
				}
			}
		} 

		*accessTime = *accessTime + L1CachePtr->access_delay;
		advanceTime(L1CachePtr->access_delay);
		L1CachePtr->accesses++;
		// cachePtr.hits++ (no such field, only for info)
	}

	else  // L1 miss
	
	{
		//printf("L1 miss\n");
		if (isBusy(L1CachePtr))
		{
			//printf("L1 miss busy\n");
			*accessTime = *accessTime + L1CachePtr->busy_cycles;
			advanceTime(L1CachePtr->busy_cycles);

		}

		if (cache_access_info.L2_hit == 1) //L2 hit
		{
			//printf("L2 hit\n");
			if (isBusy(L2CachePtr)) 
			{
				//printf("L2 hit busy\n");
				if (isInSameBlock(L2CachePtr, L2CachePtr->last_requested_address, cache_access_info.address)) {
					//printf("L2 hit busy same block\n");

					d1 = calculateCyclicDistance(L2CachePtr, L2CachePtr->last_requested_address, cache_access_info.address);
					d2 = calculateCyclicDistance(L2CachePtr, L2CachePtr->current_imported_address, cache_access_info.address);
					d = d1 - d2;

					if (d > 0) //word not yet arrived
					{
						//printf("L2 hit busy same block not arrived\n");
						*accessTime = *accessTime + d;
						advanceTime(d);
					}

				}
			}


			advanceTime(L2CachePtr->access_delay);

			//Starting to import the block; Init the varibales.
			L1CachePtr->last_requested_address = L1CachePtr->next_requested_address;
			L1CachePtr->current_imported_address = L1CachePtr->last_requested_address;

			L1CachePtr->busy_cycles = L1CachePtr->num_words_in_block;
			
			advanceTime(L1CachePtr->access_delay); //We wait L1 access delay to get the word, currently_imported_address decrements respectively

			t = L1CachePtr->access_delay + L2CachePtr->access_delay;
			*accessTime = *accessTime + t;

			L2CachePtr->accesses++;
			//L2CachePtr->hits++ (not used, for info)

			L1CachePtr->accesses++;
			L1CachePtr->misses++;
		
		}

		else //L2 miss

		{
			//printf("L2 miss\n");
			if (isBusy(L2CachePtr)) 
			{
				//printf("L2 miss busy\n");
				*accessTime = *accessTime + L2CachePtr->busy_cycles;
				advanceTime(L2CachePtr->busy_cycles);
			}

			if (cache_access_info.wasDirty == 1) //waiting for dirty block being written to memory
			{
				//printf("L2 miss was dirty\n");
				t = config_values[MEM_ACCESS_DELAY] + L2CachePtr->num_words_in_block - 1;
				advanceTime(t);
			}

			t += L1CachePtr->access_delay + L2CachePtr->access_delay + config_values[MEM_ACCESS_DELAY];

			advanceTime(config_values[MEM_ACCESS_DELAY]);
			advanceTime(L2CachePtr->access_delay);

			L2CachePtr->busy_cycles = L2CachePtr->num_words_in_block; 

			L2CachePtr->last_requested_address = L2CachePtr->next_requested_address;
			L2CachePtr->current_imported_address = L2CachePtr->last_requested_address;
			
			L1CachePtr->last_requested_address = L1CachePtr->next_requested_address;
			L1CachePtr->current_imported_address = L1CachePtr->last_requested_address;

			L1CachePtr->busy_cycles = L1CachePtr->num_words_in_block; 

			advanceTime(L1CachePtr->access_delay);
			*accessTime = *accessTime + t;

			L2CachePtr->accesses++;
			L2CachePtr->misses++;

			L1CachePtr->accesses++;
			L1CachePtr->misses++;

		}
	}

	return 0;

	}

int writeOperationTiming(int * accessTime) {

	int d1, d2, d, t, same_block_wait; //used for local calculations; exactly same names as in the algorithm in documentation

	Cache * L1CachePtr;
	Cache * L2CachePtr;

	
	L1CachePtr = &L1_data_cache; //asuming data cache; no write should be performed to instruction cache!
	L2CachePtr = L1CachePtr->nextLevelCache;

	same_block_wait = 0;

	if (write_buffer.busy_cycles > 0) {

		*accessTime = *accessTime + write_buffer.busy_cycles;
		advanceTime(write_buffer.wait_before_import); //first, wait before allocating something, if needed.
		write_buffer.wait_before_import = 0;

		//we replace requested address only after we wait previous import finished
		if (L1CachePtr->last_requested_address != L1CachePtr->next_requested_address) {
			L1CachePtr->last_requested_address = L1CachePtr->next_requested_address;
			L1CachePtr->current_imported_address = L1CachePtr->last_requested_address;
		}

		if (L2CachePtr->last_requested_address != L2CachePtr->next_requested_address) {
			L2CachePtr->current_imported_address = L2CachePtr->next_requested_address;
			L2CachePtr->current_imported_address = L2CachePtr->last_requested_address;
		}

		advanceTime(write_buffer.busy_cycles); //now we wait the remained cycles;

	}

	//Performing backgroung write allocation and write

	if (cache_access_info.L1_hit == 1) //L1 hit

	{

		if (isBusy(L1CachePtr)) 
		{
			if (isInSameBlock(L1CachePtr, L1CachePtr->last_requested_address, cache_access_info.address))
			{
				
				d1 = calculateCyclicDistance(L1CachePtr, L1CachePtr->last_requested_address, cache_access_info.address);
				d2 = calculateCyclicDistance(L1CachePtr, L1CachePtr->current_imported_address, cache_access_info.address);
				
				same_block_wait = d1 - d2;
				
				if (same_block_wait < 0) {
					same_block_wait = 0;
				}

				if (same_block_wait != 0) { //word still not arrived
					write_buffer.busy_cycles += same_block_wait; 
				}
			}

		}

		write_buffer.busy_cycles += L1CachePtr->access_delay;
		L1CachePtr->accesses++;
		//L1CachePtr->hits++ (not used, only for info)
	}

	else //L1 miss

	{
		if (isBusy(L1CachePtr)) {
			write_buffer.wait_before_import += L1CachePtr->busy_cycles;
		}

		if (cache_access_info.L2_hit == 1) //L2 hit
		{
			if (isBusy(L2CachePtr)) 
			{
				if (isInSameBlock(L2CachePtr, L2CachePtr->last_requested_address, cache_access_info.address)) 
				{
					
					d1 = calculateCyclicDistance(L2CachePtr, L2CachePtr->last_requested_address, cache_access_info.address);
					d2 = calculateCyclicDistance(L2CachePtr, L2CachePtr->current_imported_address, cache_access_info.address);
				
					same_block_wait = d1 - d2;

					if (same_block_wait < 0) {
						same_block_wait = 0;
					}

					if (same_block_wait != 0) {
						//printf("same block wait: %d\n",same_block_wait);
						write_buffer.busy_cycles += same_block_wait;
					}
				}
			}

			//Firstly assumed we should not wait for whole block for the allocation; The results are closer to the examples if we do wait.
				//d = (cache_access_info.address - alignAddress(L1CachePtr, cache_access_info.address)) / NUM_BYTES_IN_WORD;
			
			d = L1CachePtr->num_words_in_block;
			t = L2CachePtr->access_delay + L1CachePtr->access_delay + d;

			write_buffer.busy_cycles += t + write_buffer.wait_before_import;

			L1CachePtr->busy_cycles += write_buffer.wait_before_import + same_block_wait + L2CachePtr->access_delay + 
										L1CachePtr->access_delay + L1CachePtr->num_words_in_block;

			L1CachePtr->accesses++;
			L1CachePtr->misses++;

			L2CachePtr->accesses++;
			//L2CachePtr->hits++ (not used, for info only)

		}
		else //L2 miss
		{
			if (isBusy(L2CachePtr)) 
			{
				write_buffer.wait_before_import += L2CachePtr->busy_cycles;
			}

			if (cache_access_info.wasDirty == 1) {
				write_buffer.wait_before_import += config_values[MEM_ACCESS_DELAY] + L2CachePtr->num_words_in_block;// - 1;
			}
			
			//see assumption above
				//d = (cache_access_info.address - alignAddress(L1CachePtr, cache_access_info.address)) / NUM_BYTES_IN_WORD;

			d = L1CachePtr->num_words_in_block;

			write_buffer.busy_cycles += config_values[MEM_ACCESS_DELAY] + L2CachePtr->access_delay + 
										L1CachePtr->access_delay + d + write_buffer.wait_before_import;

			L2CachePtr->busy_cycles += write_buffer.wait_before_import + config_values[MEM_ACCESS_DELAY] +
										L2CachePtr->access_delay + L1CachePtr->access_delay + L2CachePtr->num_words_in_block;

			L1CachePtr->busy_cycles += write_buffer.wait_before_import + config_values[MEM_ACCESS_DELAY] +
										L2CachePtr->access_delay + L1CachePtr->access_delay + L1CachePtr->num_words_in_block;

			L1CachePtr->accesses++;
			L1CachePtr->misses++;

			L2CachePtr->accesses++;
			L2CachePtr->misses++;
		}
	}
	
	return 0;

}

//Calls the right algorithm according to type of memory access
//The algorithm is designed only for 2-level cache system. Adding more levels is not supported in this algorithm, unfortunatelly.
int cacheTimeAlgorithm() {

	int accessTime = 0;

	if (cache_access_info.isWrite == 1) {
		writeOperationTiming(&accessTime);
	}
	else {
		readOperationTiming(&accessTime);
	}

	total_memory_accesses++;
	total_memory_access_time += accessTime;

	return 0;

}


//=====================================================================================================================
//FOR RESULTS PRINT
//=====================================================================================================================

int printCache(Cache * cache, FILE * target) {
	int i, j;
	int way, block;
	j = 0;
	for (way = 0; way < cache->num_of_ways; way++) {
		for (block = 0; block < cache->num_of_blocks_per_way; block++) {
			for (i = 0; i < cache->block_size; i++) {
				fprintf(target, "%.2x", cache->cache_ways[way].cache_blocks[block].block_data[i]);
				j++;
				if ((j % BYTES_PER_LINE) == 0)
					fprintf(target, "\n");
				else
					fprintf(target, " ");
			}
			
		}
	}
	return 0;
}

double calculateHitRate(Cache * cache1, Cache * cache2) {
	double result;
	int accesses, misses;

	accesses = cache1->accesses + cache2->accesses;
	misses = cache1->misses + cache2->misses;

	if (accesses == 0) 
		result = 0.0;
	else
		result = (double)(accesses - misses) / accesses;

	return result;
}

int printHitRates(FILE * target) {

	double L1, L2;

	L1 = calculateHitRate(&L1_data_cache, &L1_inst_cache);
	L2 = calculateHitRate(&L2_data_cache, &L2_inst_cache);
	fprintf(target, "L1 %d\n", (int)(L1*100));
	fprintf(target, "L2 %d\n", (int)(L2*100));

	fprintf(target, "AMAT %.2f\n", (double)total_memory_access_time/(double)total_memory_accesses);
	
	return 0;
}

//=====================================================================================================================
//HIGH LEVEL CACHE API
//=====================================================================================================================

int fetchInstructionFromCache(int inst_index) {

	resetCacheAccessInfo();

	cache_access_info.isInstruction = 1;
	cache_access_info.isWrite = 0;

	cache_access_info.address =  inst_address(inst_index);

	getWordFromCache(&L1_inst_cache, inst_address(inst_index), NULL);

	cacheTimeAlgorithm();

	return 0;
}

int fetchDataFromCache(unsigned char * target, int address) {
	
	resetCacheAccessInfo();

	cache_access_info.isInstruction = 0;
	cache_access_info.isWrite = 0;

	cache_access_info.address = address;

	getWordFromCache(&L1_data_cache, address, target);

	cacheTimeAlgorithm();

	return 0;
}

int storeDataToCache(unsigned char * buffer, int address) {

	resetCacheAccessInfo();

	cache_access_info.isInstruction = 0;
	cache_access_info.isWrite = 1;

	cache_access_info.address = address;

	getWordFromCache(&L1_data_cache, address, NULL);
	writeWordToCache(&L1_data_cache,buffer,address);

	cacheTimeAlgorithm();

	return 0;
}

